﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Borg.AttributeAnalyze;
using Borg.AttributeAnalyze.Result;
using Properties;

namespace Borg
{
    [Extension(OnCreated = "OnCreated")]
    [ParamValue("SupportedExtensions", ".dll;.zip;.xap", new string[] { })]
    [Plug("Analyzer")]
    public class AssemblyAnalyzer : IAnalyzer
    {
        private static readonly IDictionary<Type, IDictionary<Type, IList<Attribute>>> AllAttributes =
            new Dictionary<Type, IDictionary<Type, IList<Attribute>>>();

        private ExtensionInfo _ext;
        private readonly LogEventHandler _log;

        public static event EventHandler<AssemblyResolvedEventArgs> OnAssemblyResolved;

        public AssemblyAnalyzer()
        {
            _log = OnLogEvent;
        }

        public AssemblyAnalyzer(LogEventHandler logHandler)
        {
            _log = logHandler;
        }

        private static void AddAttribute(Dictionary<Type, IList<Attribute>> attributes, Attribute attr)
        {
            for (var type = attr.GetType(); typeof(Attribute).IsAssignableFrom(type); type = type.BaseType)
            {
                IList<Attribute> attrs;

                if (!attributes.TryGetValue(type, out attrs))
                {
                    attrs = new List<Attribute>();
                    attributes.Add(type, attrs);
                }

                attrs.Add(attr);
            }
        }

        public ContractInfo AnalyzeContract(FileInfo fileInfo, Assembly assembly, TypeBuilder builder)
        {
            if (!IsAssemblyContract(assembly))
                return null;

            _log(this, new LogEventArgs(
                           MessageType.Info,
                           VerbosityLevel.Detailed,
                           Messages.ContractDiscovered,
                           new object[]
                               {
                                   fileInfo.Name,
                                   string.Format("\n  ({0})",
                                                 fileInfo.FullName)
                               }));

            var slotDefs = new List<SlotDefinition>();

            foreach (var type in assembly.GetTypes())
            {
                var customAttrs = GetCustomAttributes<SlotDefinitionAttribute>(type);

                if (customAttrs.Count == 0)
                    continue;

                var paramAttrs = GetCustomAttributes<ParamAttribute>(type);

                foreach (var slotDefAttr in customAttrs)
                {
                    _log(this,
                         new LogEventArgs(
                             MessageType.Info,
                             VerbosityLevel.Diagnostic,
                             Messages.SlotDefinitionFound,
                             new object[] { slotDefAttr.Name }));

                    var paramDefs = new List<ParamDefinition>();

                    foreach (var paramAttr in paramAttrs)
                    {
                        _log(this,
                             new LogEventArgs(
                                 MessageType.Info,
                                 VerbosityLevel.Diagnostic,
                                 Messages.ParamDefinitionFound,
                                 new object[] { paramAttr.Name }));

                        paramDefs.Add(builder.CreateParamDefinition(paramAttr));
                    }

                    slotDefs.Add(builder.CreateSlotDefinition(slotDefAttr, type, paramDefs));
                }
            }

            var repoId = new RepositoryId(new Uri(fileInfo.FullName), fileInfo.Name, fileInfo.LastWriteTime);

            return builder.CreateContract(repoId, assembly, slotDefs);
        }

        public bool AnalyzeFile(string file, TypeBuilder builder, out ContractInfo[] contractInfos, out PluginInfo[] pluginInfos)
        {
            var cis = new List<ContractInfo>();
            var pis = new List<PluginInfo>();

            if (File.Exists(file))
            {
                var fileInfo = new FileInfo(file);
                var ext = fileInfo.Extension.ToLower();

                if (ext == ".dll")
                {
                    var asm = GetAssembly(fileInfo);

                    if (!ShouldIgnore(asm))
                    {
                        var ci = AnalyzeContract(fileInfo, asm, builder);
                        var pi = AnalyzePlugin(fileInfo, asm, builder);

                        if (ci != null)
                            cis.Add(ci);

                        if (pi != null)
                            pis.Add(pi);
                    }
                }
            }

            contractInfos = cis.ToArray();
            pluginInfos = pis.ToArray();

            return (pis.Count > 0 || cis.Count > 0);
        }

        public PluginInfo AnalyzePlugin(FileInfo fileInfo, Assembly assembly, TypeBuilder builder)
        {
            if (!IsAssemblyPlugin(assembly))
                return null;

            var name = assembly.GetName();

            _log(this,
                 new LogEventArgs(
                     MessageType.Info,
                     VerbosityLevel.Detailed,
                     Messages.PluginDiscovered,
                     new object[]
                         {
                             name.Name,
                             string.Format("\n  ({0})", name.EscapedCodeBase)
                         }));

            var etis = new List<ExtensionTypeInfo>();

            foreach (var type in assembly.GetTypes())
            {
                var customAttrs = GetCustomAttributes<ExtensionAttribute>(type);

                if (customAttrs.Count == 0)
                    continue;

                var pas = GetCustomAttributes<PlugAttribute>(type);
                var sas = GetCustomAttributes<SlotAttribute>(type);
                var pvas = GetCustomAttributes<ParamValueAttribute>(type);
                var mas = GetCustomAttributes<MonitorAttribute>(type);

                foreach (var extAttr in customAttrs)
                {
                    var s = string.IsNullOrEmpty(extAttr.Name)
                        ? type.Name
                        : extAttr.Name;

                    _log(this,
                         new LogEventArgs(
                             MessageType.Info,
                             VerbosityLevel.Diagnostic,
                             Messages.ExtensionFound,
                             new object[] { s }));

                    var ptis = new List<PlugTypeInfo>();

                    foreach (var plugAttr in pas)
                    {
                        _log(this,
                             new LogEventArgs(
                                 MessageType.Info,
                                 VerbosityLevel.Diagnostic,
                                 Messages.PlugFound,
                                 new object[] { plugAttr.ForSlot }));

                        var paramValues = new List<ParamValue>();

                        foreach (var pvAttr in pvas)
                        {
                            if (!pvAttr.IsForPlug(plugAttr.ForSlot))
                                continue;

                            _log(this,
                                 new LogEventArgs(
                                     MessageType.Info,
                                     VerbosityLevel.Diagnostic,
                                     Messages.ParamValueFound,
                                     new object[] { pvAttr.Name }));

                            paramValues.Add(builder.CreateParamValue(pvAttr));
                        }

                        ptis.Add(builder.CreatePlugType(plugAttr, paramValues));
                    }

                    var slotTypes = new List<SlotTypeInfo>();

                    foreach (SlotAttribute slotAttr in sas)
                    {
                        _log(this,
                             new LogEventArgs(
                                 MessageType.Info,
                                 VerbosityLevel.Diagnostic,
                                 Messages.SlotFound,
                                 new object[] { slotAttr.Name }));

                        slotTypes.Add(builder.CreateSlotType(slotAttr));
                    }

                    var monitors = mas.Select(builder.CreateMonitor).ToList();

                    etis.Add(builder.CreateExtensionType(extAttr, type, ptis, slotTypes, monitors));
                }
            }

            var repoId = new RepositoryId(new Uri(fileInfo.FullName), fileInfo.Name, fileInfo.LastWriteTime);

            return builder.CreatePlugin(repoId, assembly, etis);
        }

        private static Assembly AssemblyResolve(object sender, ResolveEventArgs args)
        {
            Assembly asm;
            var name = new AssemblyName(args.Name);

            try
            {
                asm = Assembly.LoadFrom(name.Name + ".dll");

                if (OnAssemblyResolved != null)
                {
                    var e = new AssemblyResolvedEventArgs
                    {
                        ResolvedAssembly = new FileInfo(asm.Location)
                    };

                    OnAssemblyResolved(sender, e);
                }

                return asm;
            }
            catch
            {
                try
                {
                    if (AssemblySearchPath != null)
                    {
                        foreach (var path in AssemblySearchPath.
                            Select(srchPath => Path.Combine(srchPath, name.Name + ".dll")).Where(File.Exists))
                        {
                            asm = Assembly.LoadFrom(path);
                            if (OnAssemblyResolved != null)
                            {
                                var e =
                                    new AssemblyResolvedEventArgs
                                    {
                                        ResolvedAssembly = new FileInfo(path)
                                    };

                                OnAssemblyResolved(sender, e);
                            }

                            return asm;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(Messages.CaughtException + ex.Message);
                }

                return null;
            }
        }

        private static T DeserializeAttribute<T>(CustomAttributeData cad)
        {
            var types = new Type[cad.ConstructorArguments.Count];
            var parameters = new object[cad.ConstructorArguments.Count];

            for (var i = 0; i < parameters.Length; i++)
            {
                var arg = cad.ConstructorArguments[i];
                types[i] = Type.GetType(arg.ArgumentType.AssemblyQualifiedName);
                parameters[i] = GetValue(arg);
            }

            var type = Type.GetType(cad.Constructor.DeclaringType.AssemblyQualifiedName);
            var local = (T)type.GetConstructor(types).Invoke(parameters);

            foreach (var namedArg in cad.NamedArguments)
            {
                switch (namedArg.MemberInfo.MemberType)
                {
                    case MemberTypes.Field:
                        type.GetField(namedArg.MemberInfo.Name).
                            SetValue(local, GetValue(namedArg.TypedValue));
                        break;

                    case MemberTypes.Property:
                        type.GetProperty(namedArg.MemberInfo.Name).
                            SetValue(local, GetValue(namedArg.TypedValue), null);
                        break;
                }
            }

            return local;
        }

        public static IDictionary<Type, IList<Attribute>> GetAllCustomAttributes(Type type)
        {
            var attributes = new Dictionary<Type, IList<Attribute>>();

            if (type.Assembly.ReflectionOnly)
            {
                foreach (var data in CustomAttributeData.GetCustomAttributes(type))
                    AddAttribute(attributes, DeserializeAttribute<Attribute>(data));

                return attributes;
            }

            foreach (var attribute in Attribute.GetCustomAttributes(type))
                AddAttribute(attributes, attribute);

            return attributes;
        }

        public static Assembly GetAssembly(FileInfo file)
        {
            var asm = Assembly.ReflectionOnlyLoadFrom(file.FullName);
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += OnReflectionOnlyAssemblyResolve;
            return asm;
        }

        public static Assembly GetAssembly(byte[] rawData)
        {
            Assembly asm;

            try { asm = Assembly.ReflectionOnlyLoad(rawData); }
            catch (Exception) { asm = null; }

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += OnReflectionOnlyAssemblyResolve;
            return asm;
        }

        public static Assembly GetAssembly(Stream stream)
        {
            Assembly asm;

            try
            {
                var buffer = new byte[stream.Length];
                stream.Read(buffer, 0, (int)stream.Length);
                asm = GetAssembly(buffer);
            }
            finally
            {
                stream.Close();
            }

            return asm;
        }

        public ContractInfo[] GetContracts(FileInfo fileInfo, TypeBuilder builder)
        {
            if (fileInfo == null)
                return null;

            var ext = fileInfo.Extension.ToLower();

            if (ext == ".dll")
            {
                var asm = GetAssembly(fileInfo);
                var ci = AnalyzeContract(fileInfo, asm, builder);

                return ci == null
                           ? new ContractInfo[0]
                           : new[] { ci };
            }

            return new ContractInfo[0];
        }

        public ContractInfo[] GetContracts(string file, TypeBuilder builder)
        {
            return GetContracts(new FileInfo(file), builder);
        }

        public static List<T> GetCustomAttributes<T>(Type type)
            where T : Attribute
        {
            IDictionary<Type, IList<Attribute>> allCustomAttributes;
            IList<Attribute> attributes;

            if (!AllAttributes.TryGetValue(type, out allCustomAttributes))
            {
                allCustomAttributes = GetAllCustomAttributes(type);
                AllAttributes.Add(type, allCustomAttributes);
            }

            var list = new List<T>();

            if (allCustomAttributes.TryGetValue(typeof(T), out attributes))
                list.AddRange(allCustomAttributes[typeof(T)].Cast<T>());

            return list;
        }

        public IEnumerable<MissingAttribute> GetMissingAtributes(FileInfo file)
        {
            InstallAssemblyResolveHandler();

            var missingAttributes = new List<MissingAttribute>();
            var asm = GetAssembly(file);

            foreach (var type in asm.GetTypes())
            {
                var customAttributes = GetCustomAttributes<ExtensionAttribute>(type);
                var slotAttributes = GetCustomAttributes<SlotAttribute>(type);
                var plugAttributes = GetCustomAttributes<PlugAttribute>(type);

                if (customAttributes.Count == 0 && slotAttributes.Count == 0 && plugAttributes.Count == 0)
                    continue;

                if (customAttributes.Count > 0 && slotAttributes.Count == 0 && plugAttributes.Count == 0)
                    missingAttributes.Add(new MissingAttribute(file, ResultType.MissingSlotOrPlugAttribute, type));

                else if ((slotAttributes.Count > 0 || plugAttributes.Count > 0) && customAttributes.Count == 0)
                    missingAttributes.Add(new MissingAttribute(file, ResultType.MissingExtensionAttribute, type));
            }

            UninstallAssemblyResolveHandler();
            return missingAttributes;
        }

        public PluginInfo[] GetPlugins(FileInfo fileInfo, TypeBuilder builder)
        {
            if (fileInfo == null)
                return null;

            var ext = fileInfo.Extension.ToLower();

            if (ext == ".dll")
            {
                var asm = GetAssembly(fileInfo);
                var plugin = AnalyzePlugin(fileInfo, asm, builder);

                return plugin == null
                    ? new PluginInfo[0]
                    : new[] { plugin };
            }

            return new PluginInfo[0];
        }

        public PluginInfo[] GetPlugins(string file, TypeBuilder builder)
        {
            return GetPlugins(new FileInfo(file), builder);
        }

        private static object GetValue(CustomAttributeTypedArgument arg)
        {
            var valueType = arg.Value.GetType();
            var argType = arg.ArgumentType;

            if (argType.IsAssignableFrom(valueType))
                return arg.Value;

            var type = Type.GetType(argType.AssemblyQualifiedName);

            if (argType.IsArray)
            {
                if (argType.GetArrayRank() != 1)
                    throw new ArrayTypeMismatchException("Only one-dimensional arrays are supported.");

                var list = arg.Value as IList;

                if (list != null)
                {
                    var count = list.Count;
                    var val = type.GetConstructor(new[] { typeof(int) }).Invoke(new object[] { count });
                    var valList = val as IList;

                    for (var i = 0; i < count; i++)
                        if (valList != null)
                            valList[i] = GetValue((CustomAttributeTypedArgument)list[i]);

                    return val;
                }
            }

            if (!argType.IsEnum)
                throw new Exception("Unable to obtain value of " + arg);

            var argVal = (int)arg.Value;

            return Enum.Parse(type, argVal.ToString(CultureInfo.InvariantCulture));
        }

        public static bool HasAssemblyAttribute(Assembly assembly, Type attributeType)
        {
            return CustomAttributeData.GetCustomAttributes(assembly).
                Any(data => attributeType.AssemblyQualifiedName ==
                    data.Constructor.DeclaringType.AssemblyQualifiedName);
        }

        private static bool HasCustomAttribute<T>(MemberInfo type)
        {
            return CustomAttributeData.GetCustomAttributes(type).
                Any(data => data.Constructor.DeclaringType.AssemblyQualifiedName ==
                    typeof(T).AssemblyQualifiedName);
        }

        private static void InstallAssemblyResolveHandler()
        {
            AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
        }

        public static bool IsAssemblyContract(Assembly assembly)
        {
            return assembly != null &&
                assembly.GetTypes().Any(HasCustomAttribute<SlotDefinitionAttribute>);
        }

        public static bool IsAssemblyPlugin(Assembly assembly)
        {
            return assembly != null &&
                assembly.GetTypes().Any(HasCustomAttribute<ExtensionAttribute>);
        }

        public void OnCreated(object sender, ExtensionEventArgs args)
        {
            _ext = args.ExtensionInfo;
        }

        private void OnLogEvent(object sender, LogEventArgs args)
        {
            _ext.LogEvent(args);
        }

        private static Assembly OnReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
        {
            Assembly asm;
            Assembly result;

            try
            {
                asm = Assembly.ReflectionOnlyLoad(args.Name);

                if (OnAssemblyResolved != null)
                {
                    var e = new AssemblyResolvedEventArgs
                    {
                        ResolvedAssembly = new FileInfo(asm.Location)
                    };

                    OnAssemblyResolved(sender, e);
                }

                result = asm;
            }
            catch
            {
                asm = ResolveAssemblyFromSearchPath(args.Name);

                if (asm != null)
                {
                    if (OnAssemblyResolved != null)
                    {
                        var e = new AssemblyResolvedEventArgs
                        {
                            ResolvedAssembly = new FileInfo(asm.Location)
                        };

                        OnAssemblyResolved(sender, e);
                    }

                    return asm;
                }

                foreach (var file in
                    from dir in AppDomain.CurrentDomain.SetupInformation.PrivateBinPath.Split(new[] { ';' })
                    select new DirectoryInfo(dir) into directory
                    from file in directory.EnumerateFiles("*.dll")
                    let asmName = AssemblyName.GetAssemblyName(file.FullName)
                    where args.Name == asmName.FullName
                    select file)
                {
                    asm = Assembly.ReflectionOnlyLoadFrom(file.FullName);

                    if (OnAssemblyResolved != null)
                    {
                        var e = new AssemblyResolvedEventArgs
                        {
                            ResolvedAssembly = file
                        };

                        OnAssemblyResolved(sender, e);
                    }

                    return asm;
                }

                throw;
            }

            return result;
        }

        private static Assembly ResolveAssemblyFromSearchPath(string assembly)
        {
            try
            {
                if (AssemblySearchPath != null)
                    foreach (var path in
                        from srchPath in AssemblySearchPath
                        let name = new AssemblyName(assembly)
                        select Path.Combine(srchPath, name.Name + ".dll")
                            into path
                            where File.Exists(path)
                            select path)
                        return Assembly.ReflectionOnlyLoadFrom(path);
            }
            catch (Exception ex)
            {
                Console.WriteLine(Messages.CaughtException + ex.Message);
            }

            return null;
        }

        private static bool ShouldIgnore(Assembly assembly)
        {
            return HasAssemblyAttribute(assembly, typeof(NoAssemblyAnalyzerAttribute));
        }

        private static void UninstallAssemblyResolveHandler()
        {
            AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve;
        }

        public static IList<string> AssemblySearchPath { get; set; }
    }
}

